Day 19: Stream API
The Stream API is a tool for processing collection data declaratively. Instead of for loops, you simply describe “what to do.” Like a factory conveyor belt, data flows through a pipeline being transformed, filtered, and aggregated.
Stream Basics
The basic flow from stream creation through intermediate operations to terminal operations.
import java.util.Arrays;
import java.util.List;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class StreamBasic {
public static void main(String[] args) {
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
// Basic pipeline: source -> intermediate ops -> terminal op
int sumOfEven = numbers.stream() // 1. Create stream
.filter(n -> n % 2 == 0) // 2. Intermediate: even only
.mapToInt(Integer::intValue) // 3. Intermediate: convert to int
.sum(); // 4. Terminal: sum
System.out.println("Even sum: " + sumOfEven); // 30
// Ways to create streams
// 1. From collections
List<String> names = List.of("Alice", "Bob", "Charlie");
Stream<String> nameStream = names.stream();
// 2. From arrays
int[] arr = {1, 2, 3, 4, 5};
IntStream intStream = Arrays.stream(arr);
// 3. Stream.of()
Stream<String> stream = Stream.of("A", "B", "C");
// 4. Range generation
IntStream range = IntStream.rangeClosed(1, 100); // 1~100
// 5. Infinite stream
Stream<Double> randoms = Stream.generate(Math::random).limit(5);
randoms.forEach(r -> System.out.printf("%.4f ", r));
System.out.println();
// Streams can only be used once! (not reusable)
Stream<Integer> oneTime = numbers.stream();
oneTime.forEach(System.out::print);
// oneTime.count(); // IllegalStateException!
}
}
Intermediate Operations
Operations that transform and filter data. They are lazily evaluated.
import java.util.List;
import java.util.stream.Stream;
record Product(String name, String category, int price) {}
public class IntermediateOps {
public static void main(String[] args) {
List<Product> products = List.of(
new Product("Laptop", "Electronics", 1500000),
new Product("Mouse", "Electronics", 35000),
new Product("Keyboard", "Electronics", 89000),
new Product("Desk", "Furniture", 250000),
new Product("Chair", "Furniture", 350000),
new Product("Monitor", "Electronics", 450000),
new Product("Book", "Stationery", 15000),
new Product("Pen", "Stationery", 3000)
);
// filter: pass only elements matching the condition
System.out.println("=== 100,000 or more ===");
products.stream()
.filter(p -> p.price() >= 100000)
.forEach(p -> System.out.println(p.name() + ": " + p.price()));
// map: transform elements
System.out.println("\n=== Product names ===");
products.stream()
.map(Product::name)
.forEach(System.out::println);
// distinct: remove duplicates
System.out.println("\n=== Category types ===");
products.stream()
.map(Product::category)
.distinct()
.forEach(System.out::println);
// sorted: sort
System.out.println("\n=== Sorted by price ===");
products.stream()
.sorted((a, b) -> Integer.compare(a.price(), b.price()))
.forEach(p -> System.out.printf("%-10s %,d won%n", p.name(), p.price()));
// peek: for debugging (inspect values mid-pipeline)
System.out.println("\n=== Pipeline trace ===");
long count = products.stream()
.filter(p -> p.price() > 50000)
.peek(p -> System.out.println("Passed filter: " + p.name()))
.map(Product::name)
.count();
System.out.println("Result count: " + count);
// flatMap: flatten nested structures
List<List<String>> nested = List.of(
List.of("a", "b"),
List.of("c", "d"),
List.of("e", "f")
);
nested.stream()
.flatMap(List::stream) // List<List<String>> -> Stream<String>
.forEach(s -> System.out.print(s + " "));
System.out.println();
}
}
Terminal Operations
Operations that consume the stream to produce a result.
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
record Employee(String name, String department, int salary) {}
public class TerminalOps {
public static void main(String[] args) {
List<Employee> employees = List.of(
new Employee("Alice", "Development", 5000),
new Employee("Bob", "Planning", 4500),
new Employee("Charlie", "Development", 6000),
new Employee("Diana", "Design", 4800),
new Employee("Eve", "Development", 5500),
new Employee("Frank", "Planning", 4200)
);
// count: number of elements
long devCount = employees.stream()
.filter(e -> e.department().equals("Development"))
.count();
System.out.println("Development team size: " + devCount);
// sum, average, min, max
int totalSalary = employees.stream()
.mapToInt(Employee::salary)
.sum();
System.out.println("Total salary: " + totalSalary);
OptionalInt maxSalary = employees.stream()
.mapToInt(Employee::salary)
.max();
System.out.println("Max salary: " + maxSalary.orElse(0));
double avgSalary = employees.stream()
.mapToInt(Employee::salary)
.average()
.orElse(0);
System.out.printf("Average salary: %.0f%n", avgSalary);
// findFirst, findAny
Optional<Employee> firstDev = employees.stream()
.filter(e -> e.department().equals("Development"))
.findFirst();
firstDev.ifPresent(e -> System.out.println("First developer: " + e.name()));
// anyMatch, allMatch, noneMatch
boolean hasHighPaid = employees.stream()
.anyMatch(e -> e.salary() > 5500);
System.out.println("Anyone above 5500? " + hasHighPaid);
boolean allPositive = employees.stream()
.allMatch(e -> e.salary() > 0);
System.out.println("All positive? " + allPositive);
// reduce: cumulative operation
int sumReduce = employees.stream()
.map(Employee::salary)
.reduce(0, Integer::sum);
System.out.println("Reduce sum: " + sumReduce);
}
}
Collecting with Collectors
Collect stream results into various collections or formats.
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
record Student(String name, int score, String grade) {}
public class CollectorsExample {
public static void main(String[] args) {
List<Student> students = List.of(
new Student("Alice", 85, "B"),
new Student("Bob", 92, "A"),
new Student("Charlie", 78, "C"),
new Student("Diana", 95, "A"),
new Student("Eve", 88, "B"),
new Student("Frank", 91, "A")
);
// toList, toSet
List<String> nameList = students.stream()
.map(Student::name)
.collect(Collectors.toList());
Set<String> gradeSet = students.stream()
.map(Student::grade)
.collect(Collectors.toSet());
System.out.println("Names: " + nameList);
System.out.println("Grade types: " + gradeSet);
// joining: string concatenation
String allNames = students.stream()
.map(Student::name)
.collect(Collectors.joining(", ", "[", "]"));
System.out.println("All: " + allNames);
// groupingBy: grouping
Map<String, List<Student>> byGrade = students.stream()
.collect(Collectors.groupingBy(Student::grade));
byGrade.forEach((grade, list) -> {
System.out.println("Grade " + grade + ": " +
list.stream().map(Student::name).collect(Collectors.joining(", ")));
});
// partitioningBy: split into two groups
Map<Boolean, List<Student>> partition = students.stream()
.collect(Collectors.partitioningBy(s -> s.score() >= 90));
System.out.println("90+ : " +
partition.get(true).stream().map(Student::name).toList());
System.out.println("Below 90: " +
partition.get(false).stream().map(Student::name).toList());
// Statistics
var stats = students.stream()
.collect(Collectors.summarizingInt(Student::score));
System.out.println("Average: " + stats.getAverage());
System.out.println("Max: " + stats.getMax());
System.out.println("Min: " + stats.getMin());
System.out.println("Sum: " + stats.getSum());
System.out.println("Count: " + stats.getCount());
}
}
Today’s Exercises
-
Sales Analysis: From a product list (name, category, price, quantity), use streams to calculate total sales per category (price x quantity), overall average price, and the most expensive product.
-
String Processing Pipeline: Extract all words from a list of sentences (flatMap), convert to lowercase, filter only words with 3+ characters, remove duplicates, sort alphabetically, and collect into a final list.
-
Employee Statistics Report: From an employee list, use streams to find the average salary per department, the highest-paid person per department, and the top 3 by salary overall, then print as a report.